/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Haystack Software Inc. All rights reserved.
 *  Licensed under the PolyForm Strict License 1.0.0. See License.txt in the project root for
 *  license information.
 *--------------------------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See code-license.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { localize } from "vs/nls"
import { URI } from "vs/base/common/uri"
import { TextResourceEditorInput } from "vs/workbench/common/editor/textResourceEditorInput"
import {
  ITextModelService,
  ITextModelContentProvider,
} from "vs/editor/common/services/resolverService"
import { ITextModel } from "vs/editor/common/model"
import {
  ILifecycleService,
  LifecyclePhase,
  StartupKindToString,
} from "vs/workbench/services/lifecycle/common/lifecycle"
import { ILanguageService } from "vs/editor/common/languages/language"
import { IInstantiationService } from "vs/platform/instantiation/common/instantiation"
import { IModelService } from "vs/editor/common/services/model"
import { ITimerService } from "vs/workbench/services/timer/browser/timerService"
import { IExtensionService } from "vs/workbench/services/extensions/common/extensions"
import { IDisposable, dispose } from "vs/base/common/lifecycle"
import { ICodeEditorService } from "vs/editor/browser/services/codeEditorService"
import { writeTransientState } from "vs/workbench/contrib/codeEditor/browser/toggleWordWrap"
import { LoaderEventType, LoaderStats, isESM } from "vs/base/common/amd"
import { IProductService } from "vs/platform/product/common/productService"
import { ITextFileService } from "vs/workbench/services/textfile/common/textfiles"
import { IEditorService } from "vs/workbench/services/editor/common/editorService"
import { ByteSize, IFileService } from "vs/platform/files/common/files"
import { ILabelService } from "vs/platform/label/common/label"
import { isWeb } from "vs/base/common/platform"
import { IFilesConfigurationService } from "vs/workbench/services/filesConfiguration/common/filesConfigurationService"
import { ITerminalService } from "vs/workbench/contrib/terminal/browser/terminal"
import * as perf from "vs/base/common/performance"
import { ITextResourceConfigurationService } from "vs/editor/common/services/textResourceConfiguration"
import { Registry } from "vs/platform/registry/common/platform"
import {
  IWorkbenchContributionsRegistry,
  Extensions as WorkbenchExtensions,
  getWorkbenchContribution,
} from "vs/workbench/common/contributions"
import { ICustomEditorLabelService } from "vs/workbench/services/editor/common/customEditorLabelService"

export class PerfviewContrib {
  static get() {
    return getWorkbenchContribution<PerfviewContrib>(PerfviewContrib.ID)
  }

  static readonly ID = "workbench.contrib.perfview"

  private readonly _inputUri = URI.from({
    scheme: "perf",
    path: "Startup Performance",
  })
  private readonly _registration: IDisposable

  constructor(
    @IInstantiationService
    private readonly _instaService: IInstantiationService,
    @ITextModelService textModelResolverService: ITextModelService
  ) {
    this._registration =
      textModelResolverService.registerTextModelContentProvider(
        "perf",
        _instaService.createInstance(PerfModelContentProvider)
      )
  }

  dispose(): void {
    this._registration.dispose()
  }

  getInputUri(): URI {
    return this._inputUri
  }

  getEditorInput(): PerfviewInput {
    return this._instaService.createInstance(PerfviewInput)
  }
}

export class PerfviewInput extends TextResourceEditorInput {
  static readonly Id = "PerfviewInput"

  override get typeId(): string {
    return PerfviewInput.Id
  }

  constructor(
    @ITextModelService textModelResolverService: ITextModelService,
    @ITextFileService textFileService: ITextFileService,
    @IEditorService editorService: IEditorService,
    @IFileService fileService: IFileService,
    @ILabelService labelService: ILabelService,
    @IFilesConfigurationService
    filesConfigurationService: IFilesConfigurationService,
    @ITextResourceConfigurationService
    textResourceConfigurationService: ITextResourceConfigurationService,
    @ICustomEditorLabelService
    customEditorLabelService: ICustomEditorLabelService
  ) {
    super(
      PerfviewContrib.get().getInputUri(),
      localize("name", "Startup Performance"),
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      textModelResolverService,
      textFileService,
      editorService,
      fileService,
      labelService,
      filesConfigurationService,
      textResourceConfigurationService,
      customEditorLabelService
    )
  }
}

class PerfModelContentProvider implements ITextModelContentProvider {
  private _model: ITextModel | undefined
  private _modelDisposables: IDisposable[] = []

  constructor(
    @IModelService private readonly _modelService: IModelService,
    @ILanguageService private readonly _languageService: ILanguageService,
    @ICodeEditorService private readonly _editorService: ICodeEditorService,
    @ILifecycleService private readonly _lifecycleService: ILifecycleService,
    @ITimerService private readonly _timerService: ITimerService,
    @IExtensionService private readonly _extensionService: IExtensionService,
    @IProductService private readonly _productService: IProductService,
    @ITerminalService private readonly _terminalService: ITerminalService
  ) {}

  provideTextContent(resource: URI): Promise<ITextModel> {
    if (!this._model || this._model.isDisposed()) {
      dispose(this._modelDisposables)
      const langId = this._languageService.createById("markdown")
      this._model =
        this._modelService.getModel(resource) ||
        this._modelService.createModel("Loading...", langId, resource)

      this._modelDisposables.push(
        langId.onDidChange((e) => {
          this._model?.setLanguage(e)
        })
      )
      this._modelDisposables.push(
        this._extensionService.onDidChangeExtensionsStatus(
          this._updateModel,
          this
        )
      )

      writeTransientState(
        this._model,
        { wordWrapOverride: "off" },
        this._editorService
      )
    }
    this._updateModel()
    return Promise.resolve(this._model)
  }

  private _updateModel(): void {
    Promise.all([
      this._timerService.whenReady(),
      this._lifecycleService.when(LifecyclePhase.Eventually),
      this._extensionService.whenInstalledExtensionsRegistered(),
      this._terminalService.whenConnected,
    ]).then(() => {
      if (this._model && !this._model.isDisposed()) {
        const stats = LoaderStats.get()
        const md = new MarkdownBuilder()
        this._addSummary(md)
        md.blank()
        this._addSummaryTable(md, stats)
        md.blank()
        this._addExtensionsTable(md)
        md.blank()
        this._addPerfMarksTable(
          "Terminal Stats",
          md,
          this._timerService
            .getPerformanceMarks()
            .find((e) => e[0] === "renderer")?.[1]
            .filter((e) => e.name.startsWith("code/terminal/"))
        )
        md.blank()
        this._addWorkbenchContributionsPerfMarksTable(md)
        md.blank()
        this._addRawPerfMarks(md)
        if (!isESM) {
          md.blank()
          this._addLoaderStats(md, stats)
          md.blank()
          this._addCachedDataStats(md)
        }
        md.blank()
        this._addResourceTimingStats(md)

        this._model.setValue(md.value)
      }
    })
  }

  private _addSummary(md: MarkdownBuilder): void {
    const metrics = this._timerService.startupMetrics
    md.heading(2, "System Info")
    md.li(
      `${this._productService.nameShort}: ${this._productService.version} (${
        this._productService.commit || "0000000"
      })`
    )
    md.li(`OS: ${metrics.platform}(${metrics.release})`)
    if (metrics.cpus) {
      md.li(
        `CPUs: ${metrics.cpus.model}(${metrics.cpus.count} x ${metrics.cpus.speed})`
      )
    }
    if (
      typeof metrics.totalmem === "number" &&
      typeof metrics.freemem === "number"
    ) {
      md.li(
        `Memory(System): ${(metrics.totalmem / ByteSize.GB).toFixed(2)} GB(${(
          metrics.freemem / ByteSize.GB
        ).toFixed(2)}GB free)`
      )
    }
    if (metrics.meminfo) {
      md.li(
        `Memory(Process): ${(
          metrics.meminfo.workingSetSize / ByteSize.KB
        ).toFixed(2)} MB working set(${(
          metrics.meminfo.privateBytes / ByteSize.KB
        ).toFixed(2)}MB private, ${(
          metrics.meminfo.sharedBytes / ByteSize.KB
        ).toFixed(2)}MB shared)`
      )
    }
    md.li(`VM(likelihood): ${metrics.isVMLikelyhood}%`)
    md.li(`Initial Startup: ${metrics.initialStartup}`)
    md.li(`Has ${metrics.windowCount - 1} other windows`)
    md.li(`Screen Reader Active: ${metrics.hasAccessibilitySupport}`)
    md.li(`Empty Workspace: ${metrics.emptyWorkbench}`)
  }

  private _addSummaryTable(md: MarkdownBuilder, stats?: LoaderStats): void {
    const metrics = this._timerService.startupMetrics
    const contribTimings = Registry.as<IWorkbenchContributionsRegistry>(
      WorkbenchExtensions.Workbench
    ).timings

    const table: Array<Array<string | number | undefined>> = []
    table.push([
      "start => app.isReady",
      metrics.timers.ellapsedAppReady,
      "[main]",
      `initial startup: ${metrics.initialStartup}`,
    ])
    table.push([
      "nls:start => nls:end",
      metrics.timers.ellapsedNlsGeneration,
      "[main]",
      `initial startup: ${metrics.initialStartup}`,
    ])
    table.push([
      "require(main.bundle.js)",
      metrics.timers.ellapsedLoadMainBundle,
      "[main]",
      `initial startup: ${metrics.initialStartup}`,
    ])
    table.push([
      "start crash reporter",
      metrics.timers.ellapsedCrashReporter,
      "[main]",
      `initial startup: ${metrics.initialStartup}`,
    ])
    table.push([
      "serve main IPC handle",
      metrics.timers.ellapsedMainServer,
      "[main]",
      `initial startup: ${metrics.initialStartup}`,
    ])
    table.push([
      "create window",
      metrics.timers.ellapsedWindowCreate,
      "[main]",
      `initial startup: ${metrics.initialStartup}, ${
        metrics.initialStartup
          ? `state: ${metrics.timers.ellapsedWindowRestoreState}ms, widget: ${metrics.timers.ellapsedBrowserWindowCreate}ms, show: ${metrics.timers.ellapsedWindowMaximize}ms`
          : ""
      }`,
    ])
    table.push([
      "app.isReady => window.loadUrl()",
      metrics.timers.ellapsedWindowLoad,
      "[main]",
      `initial startup: ${metrics.initialStartup}`,
    ])
    table.push([
      "window.loadUrl() => begin to require(workbench.desktop.main.js)",
      metrics.timers.ellapsedWindowLoadToRequire,
      "[main->renderer]",
      StartupKindToString(metrics.windowKind),
    ])
    table.push([
      "require(workbench.desktop.main.js)",
      metrics.timers.ellapsedRequire,
      "[renderer]",
      `cached data: ${metrics.didUseCachedData ? "YES" : "NO"}${
        stats ? `, node_modules took ${stats.nodeRequireTotal}ms` : ""
      }`,
    ])
    table.push([
      "wait for window config",
      metrics.timers.ellapsedWaitForWindowConfig,
      "[renderer]",
      undefined,
    ])
    table.push([
      "init storage (global & workspace)",
      metrics.timers.ellapsedStorageInit,
      "[renderer]",
      undefined,
    ])
    table.push([
      "init workspace service",
      metrics.timers.ellapsedWorkspaceServiceInit,
      "[renderer]",
      undefined,
    ])
    if (isWeb) {
      table.push([
        "init settings and global state from settings sync service",
        metrics.timers.ellapsedRequiredUserDataInit,
        "[renderer]",
        undefined,
      ])
      table.push([
        "init keybindings, snippets & extensions from settings sync service",
        metrics.timers.ellapsedOtherUserDataInit,
        "[renderer]",
        undefined,
      ])
    }
    table.push([
      "register extensions & spawn extension host",
      metrics.timers.ellapsedExtensions,
      "[renderer]",
      undefined,
    ])
    table.push([
      "restore viewlet",
      metrics.timers.ellapsedViewletRestore,
      "[renderer]",
      metrics.viewletId,
    ])
    table.push([
      "restore panel",
      metrics.timers.ellapsedPanelRestore,
      "[renderer]",
      metrics.panelId,
    ])
    table.push([
      "restore & resolve visible editors",
      metrics.timers.ellapsedEditorRestore,
      "[renderer]",
      `${metrics.editorIds.length}: ${metrics.editorIds.join(", ")}`,
    ])
    table.push([
      "create workbench contributions",
      metrics.timers.ellapsedWorkbenchContributions,
      "[renderer]",
      `${
        (contribTimings.get(LifecyclePhase.Starting)?.length ?? 0) +
        (contribTimings.get(LifecyclePhase.Starting)?.length ?? 0)
      } blocking startup`,
    ])
    table.push([
      "overall workbench load",
      metrics.timers.ellapsedWorkbench,
      "[renderer]",
      undefined,
    ])
    table.push([
      "workbench ready",
      metrics.ellapsed,
      "[main->renderer]",
      undefined,
    ])
    table.push([
      "renderer ready",
      metrics.timers.ellapsedRenderer,
      "[renderer]",
      undefined,
    ])
    table.push([
      "shared process connection ready",
      metrics.timers.ellapsedSharedProcesConnected,
      "[renderer->sharedprocess]",
      undefined,
    ])
    table.push([
      "extensions registered",
      metrics.timers.ellapsedExtensionsReady,
      "[renderer]",
      undefined,
    ])

    md.heading(2, "Performance Marks")
    md.table(["What", "Duration", "Process", "Info"], table)
  }

  private _addExtensionsTable(md: MarkdownBuilder): void {
    const eager: { toString(): string }[][] = []
    const normal: { toString(): string }[][] = []
    const extensionsStatus = this._extensionService.getExtensionsStatus()
    for (const id in extensionsStatus) {
      const { activationTimes: times } = extensionsStatus[id]
      if (!times) {
        continue
      }
      if (times.activationReason.startup) {
        eager.push([
          id,
          times.activationReason.startup,
          times.codeLoadingTime,
          times.activateCallTime,
          times.activateResolvedTime,
          times.activationReason.activationEvent,
          times.activationReason.extensionId.value,
        ])
      } else {
        normal.push([
          id,
          times.activationReason.startup,
          times.codeLoadingTime,
          times.activateCallTime,
          times.activateResolvedTime,
          times.activationReason.activationEvent,
          times.activationReason.extensionId.value,
        ])
      }
    }

    const table = eager.concat(normal)
    if (table.length > 0) {
      md.heading(2, "Extension Activation Stats")
      md.table(
        [
          "Extension",
          "Eager",
          "Load Code",
          "Call Activate",
          "Finish Activate",
          "Event",
          "By",
        ],
        table
      )
    }
  }

  private _addPerfMarksTable(
    name: string | undefined,
    md: MarkdownBuilder,
    marks: readonly perf.PerformanceMark[] | undefined
  ): void {
    if (!marks) {
      return
    }
    const table: Array<Array<string | number | undefined>> = []
    let lastStartTime = -1
    let total = 0
    for (const { name, startTime } of marks) {
      const delta = lastStartTime !== -1 ? startTime - lastStartTime : 0
      total += delta
      table.push([
        name,
        Math.round(startTime),
        Math.round(delta),
        Math.round(total),
      ])
      lastStartTime = startTime
    }
    if (name) {
      md.heading(2, name)
    }
    md.table(["Name", "Timestamp", "Delta", "Total"], table)
  }

  private _addWorkbenchContributionsPerfMarksTable(md: MarkdownBuilder): void {
    md.heading(2, "Workbench Contributions Blocking Restore")

    const timings = Registry.as<IWorkbenchContributionsRegistry>(
      WorkbenchExtensions.Workbench
    ).timings
    md.li(
      `Total (LifecyclePhase.Starting): ${
        timings.get(LifecyclePhase.Starting)?.length
      } (${timings
        .get(LifecyclePhase.Starting)
        ?.reduce((p, c) => p + c[1], 0)}ms)`
    )
    md.li(
      `Total (LifecyclePhase.Ready): ${
        timings.get(LifecyclePhase.Ready)?.length
      } (${timings.get(LifecyclePhase.Ready)?.reduce((p, c) => p + c[1], 0)}ms)`
    )
    md.blank()

    const marks = this._timerService
      .getPerformanceMarks()
      .find((e) => e[0] === "renderer")?.[1]
      .filter(
        (e) =>
          e.name.startsWith("code/willCreateWorkbenchContribution/1") ||
          e.name.startsWith("code/didCreateWorkbenchContribution/1") ||
          e.name.startsWith("code/willCreateWorkbenchContribution/2") ||
          e.name.startsWith("code/didCreateWorkbenchContribution/2")
      )
    this._addPerfMarksTable(undefined, md, marks)
  }

  private _addRawPerfMarks(md: MarkdownBuilder): void {
    for (const [source, marks] of this._timerService.getPerformanceMarks()) {
      md.heading(2, `Raw Perf Marks: ${source}`)
      md.value += "```\n"
      md.value += `Name\tTimestamp\tDelta\tTotal\n`
      let lastStartTime = -1
      let total = 0
      for (const { name, startTime } of marks) {
        const delta = lastStartTime !== -1 ? startTime - lastStartTime : 0
        total += delta
        md.value += `${name}\t${startTime}\t${delta}\t${total}\n`
        lastStartTime = startTime
      }
      md.value += "```\n"
    }
  }

  private _addLoaderStats(md: MarkdownBuilder, stats: LoaderStats): void {
    md.heading(2, "Loader Stats")
    md.heading(3, "Load AMD-module")
    md.table(["Module", "Duration"], stats.amdLoad)
    md.blank()
    md.heading(3, "Load commonjs-module")
    md.table(["Module", "Duration"], stats.nodeRequire)
    md.blank()
    md.heading(3, "Invoke AMD-module factory")
    md.table(["Module", "Duration"], stats.amdInvoke)
    md.blank()
    md.heading(3, "Invoke commonjs-module")
    md.table(["Module", "Duration"], stats.nodeEval)
  }

  private _addCachedDataStats(md: MarkdownBuilder): void {
    const map = new Map<LoaderEventType, string[]>()
    map.set(LoaderEventType.CachedDataCreated, [])
    map.set(LoaderEventType.CachedDataFound, [])
    map.set(LoaderEventType.CachedDataMissed, [])
    map.set(LoaderEventType.CachedDataRejected, [])
    if (typeof require.getStats === "function") {
      for (const stat of require.getStats()) {
        if (map.has(stat.type)) {
          map.get(stat.type)!.push(stat.detail)
        }
      }
    }

    const printLists = (arr?: string[]) => {
      if (arr) {
        arr.sort()
        for (const e of arr) {
          md.li(`${e}`)
        }
        md.blank()
      }
    }

    md.heading(2, "Node Cached Data Stats")
    md.blank()
    md.heading(3, "cached data used")
    printLists(map.get(LoaderEventType.CachedDataFound))
    md.heading(3, "cached data missed")
    printLists(map.get(LoaderEventType.CachedDataMissed))
    md.heading(3, "cached data rejected")
    printLists(map.get(LoaderEventType.CachedDataRejected))
    md.heading(3, "cached data created (lazy, might need refreshes)")
    printLists(map.get(LoaderEventType.CachedDataCreated))
  }

  private _addResourceTimingStats(md: MarkdownBuilder) {
    const stats = performance.getEntriesByType("resource").map((entry) => {
      return [entry.name, entry.duration]
    })
    if (!stats.length) {
      return
    }
    md.heading(2, "Resource Timing Stats")
    md.table(["Name", "Duration"], stats)
  }
}

class MarkdownBuilder {
  value: string = ""

  heading(level: number, value: string): this {
    this.value += `${"#".repeat(level)} ${value}\n\n`
    return this
  }

  blank() {
    this.value += "\n"
    return this
  }

  li(value: string) {
    this.value += `* ${value}\n`
    return this
  }

  table(
    header: string[],
    rows: Array<Array<{ toString(): string } | undefined>>
  ) {
    this.value += LoaderStats.toMarkdownTable(header, rows)
  }
}
